/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.dtv.lwuit;

import org.thenesis.microbackend.ui.graphics.VirtualGraphics;
import org.thenesis.microbackend.ui.graphics.VirtualImage;

/**
 * Abstracts the underlying platform graphics context thus allowing us to
 * achieve portability between MIDP devices and CDC devices. This abstaction
 * simplifies and unifies the Graphics implementations of various platforms.
 * 
 * <p>
 * A graphics instance is never created by the developer and is always accessed
 * using either a paint callback or a mutable image. There is no way to create
 * this object directly.
 */
public final class Graphics {

    private VirtualGraphics g;
    private int xTranslate;
    private int yTranslate;
    private Font current = Font.getDefaultFont();
    /**
     * Cache value for the fillRect method
     */
    private int[] rgbArr;

    /**
     * This flag indicates if the drawRGB method is able to draw negative x and
     * y In drawRGB method, some devices such as BlackBerry throw exceptions if
     * you try to give negative values to drawRGB method.
     */
    private static boolean drawNegativeOffsetsInRGB = true;

    /**
     * Constructs a new Graphics object.
     */
    Graphics() {
    }

    /**
     * Constructing new graphics with a given javax.microedition.lcdui.Graphics
     * 
     * @param g
     *            a given javax.microedition.lcdui.Graphics
     */
    Graphics(VirtualGraphics g) {
        setGraphics(g);
    }

    /**
     * Setting graphics with a given javax.microedition.lcdui.Graphics
     * 
     * @param g
     *            a given javax.microedition.lcdui.Graphics
     */
    void setGraphics(VirtualGraphics g) {
        this.g = g;
    }

    /**
     * Returns javax.microedition.lcdui.Graphics
     * 
     * @return a javax.microedition.lcdui.Graphics object
     */
    VirtualGraphics getGraphics() {
        return g;
    }

    /**
     * Translates the X/Y location for drawing on the underlying surface.
     * Translation is incremental so the new value will be added to the current
     * translation and in order to reset translation we have to invoke {@code
     * translate(-getTranslateX(), -getTranslateY()) }
     * 
     * @param x
     *            the x coordinate
     * @param y
     *            the y coordinate
     */
    public void translate(int x, int y) {
        g.translate(x, y);
        // xTranslate += x;
        // yTranslate += y;
    }

    /**
     * Returns the current x translate value
     * 
     * @return the current x translate value
     */
    public int getTranslateX() {
        return g.getTranslateX();
        // return xTranslate;
    }

    /**
     * Returns the current y translate value
     * 
     * @return the current y translate value
     */
    public int getTranslateY() {
        return g.getTranslateY();
        // return yTranslate;
    }

    /**
     * Returns the current color
     * 
     * @return the RGB graphics color
     */
    public int getColor() {
        return g.getColor();
    }

    /**
     * Sets the current rgb color while ignoring any potential alpha component
     * within said color value.
     * 
     * @param RGB
     *            the RGB value for the color.
     */
    public void setColor(int RGB) {
        g.setColor(RGB);
    }

    /**
     * Returns the font used with the drawString method calls
     * 
     * @return the font used with the drawString method calls
     */
    public Font getFont() {
        return current;
    }

    /**
     * Sets the font to use with the drawString method calls
     * 
     * @param font
     *            the font used with the drawString method calls
     */
    public void setFont(Font font) {
        this.current = font;
    }

    // /**
    // * Sets the javax.microedition.lcdui.Font to use with the
    // * drawString method calls
    // *
    // * @param font the javax.microedition.lcdui.Font
    // */
    // void setFont(javax.microedition.lcdui.Font font) {
    // g.setFont(font);
    // }

    /**
     * Returns the x clipping position
     * 
     * @return the x clipping position
     */
    public int getClipX() {
        return g.getClipX() - xTranslate;
    }

    /**
     * Returns the y clipping position
     * 
     * @return the y clipping position
     */
    public int getClipY() {
        return g.getClipY() - yTranslate;
    }

    /**
     * Returns the clip width
     * 
     * @return the clip width
     */
    public int getClipWidth() {
        return g.getClipWidth();
    }

    /**
     * Returns the clip height
     * 
     * @return the clip height
     */
    public int getClipHeight() {
        return g.getClipHeight();
    }

    /**
     * Clips the given rectangle by intersecting with the current clipping
     * region, this method can thus only shrink the clipping region and never
     * increase it.
     * 
     * @param x
     *            the x coordinate of the rectangle to intersect the clip with
     * @param y
     *            the y coordinate of the rectangle to intersect the clip with
     * @param width
     *            the width of the rectangle to intersect the clip with
     * @param height
     *            the height of the rectangle to intersect the clip with
     */
    public void clipRect(int x, int y, int width, int height) {
        g.clipRect(xTranslate + x, yTranslate + y, width, height);
    }

    /**
     * Updates the clipping region to match the given region exactly
     * 
     * @param x
     *            the x coordinate of the new clip rectangle.
     * @param y
     *            the y coordinate of the new clip rectangle.
     * @param width
     *            the width of the new clip rectangle.
     * @param height
     *            the height of the new clip rectangle.
     */
    public void setClip(int x, int y, int width, int height) {
        g.setClip(xTranslate + x, yTranslate + y, width, height);
    }

    /**
     * Draws a line between the 2 X/Y coordinates
     * 
     * @param x1
     *            first x position
     * @param y1
     *            first y position
     * @param x2
     *            second x position
     * @param y2
     *            second y position
     */
    public void drawLine(int x1, int y1, int x2, int y2) {
        g.drawLine(xTranslate + x1, yTranslate + y1, xTranslate + x2, yTranslate + y2);
    }

    /**
     * Fills the rectangle from the given position according to the width/height
     * minus 1 pixel according to the convnetion in Java.
     * 
     * @param x
     *            the x coordinate of the rectangle to be filled.
     * @param y
     *            the y coordinate of the rectangle to be filled.
     * @param width
     *            the width of the rectangle to be filled.
     * @param height
     *            the height of the rectangle to be filled.
     */
    public void fillRect(int x, int y, int width, int height) {
        g.fillRect(xTranslate + x, yTranslate + y, width, height);
    }

    /**
     * Draws a rectangle in the given coordinates
     * 
     * @param x
     *            the x coordinate of the rectangle to be drawn.
     * @param y
     *            the y coordinate of the rectangle to be drawn.
     * @param width
     *            the width of the rectangle to be drawn.
     * @param height
     *            the height of the rectangle to be drawn.
     */
    public void drawRect(int x, int y, int width, int height) {
        g.drawRect(xTranslate + x, yTranslate + y, width, height);
    }

    /**
     * Draws a rounded corner rectangle in the given coordinates with the
     * arcWidth/height matching the last two arguments respectively.
     * 
     * @param x
     *            the x coordinate of the rectangle to be drawn.
     * @param y
     *            the y coordinate of the rectangle to be drawn.
     * @param width
     *            the width of the rectangle to be drawn.
     * @param height
     *            the height of the rectangle to be drawn.
     * @param arcWidth
     *            the horizontal diameter of the arc at the four corners.
     * @param arcHeight
     *            the vertical diameter of the arc at the four corners.
     */
    public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
        g.drawRoundRect(xTranslate + x, yTranslate + y, width, height, arcWidth, arcHeight);
    }

    /**
     * Makes the current color slightly lighter, this is useful for many visual
     * effects
     * 
     * @param factor
     *            the degree of lightening a color per channel a number from 1
     *            to 255
     */
    public void lighterColor(int factor) {
        int color = getColor();
        int r = color >> 16 & 0xff;
        int g = color >> 8 & 0xff;
        int b = color & 0xff;
        r = Math.min(0xff, r + factor);
        g = Math.min(0xff, g + factor);
        g = Math.min(0xff, b + factor);
        setColor(((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | (b & 0xff));
    }

    /**
     * Makes the current color slightly darker, this is useful for many visual
     * effects
     * 
     * @param factor
     *            the degree of lightening a color per channel a number from 1
     *            to 255
     */
    public void darkerColor(int factor) {
        int color = getColor();
        int r = color >> 16 & 0xff;
        int g = color >> 8 & 0xff;
        int b = color & 0xff;
        r = Math.max(0, r - factor);
        g = Math.max(0, g - factor);
        g = Math.max(0, b - factor);
        setColor(((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | (b & 0xff));
    }

    /**
     * Fills a rounded rectangle in the same way as drawRoundRect
     * 
     * @param x
     *            the x coordinate of the rectangle to be filled.
     * @param y
     *            the y coordinate of the rectangle to be filled.
     * @param width
     *            the width of the rectangle to be filled.
     * @param height
     *            the height of the rectangle to be filled.
     * @param arcWidth
     *            the horizontal diameter of the arc at the four corners.
     * @param arcHeight
     *            the vertical diameter of the arc at the four corners.
     * @see #drawRoundRect
     */
    public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
        g.fillRoundRect(xTranslate + x, yTranslate + y, width, height, arcWidth, arcHeight);
    }

    /**
     * Fills a circular or eliptical arc based on the given angles and bounding
     * box. The resulting arc begins at startAngle and extends for arcAngle
     * degrees.
     * 
     * @param x
     *            the x coordinate of the upper-left corner of the arc to be
     *            filled.
     * @param y
     *            the y coordinate of the upper-left corner of the arc to be
     *            filled.
     * @param width
     *            the width of the arc to be filled.
     * @param height
     *            the height of the arc to be filled.
     * @param startAngle
     *            the beginning angle.
     * @param arcAngle
     *            the angular extent of the arc, relative to the start angle.
     */
    public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        g.fillArc(xTranslate + x, yTranslate + y, width, height, startAngle, arcAngle);
    }

    /**
     * Draws a circular or eliptical arc based on the given angles and bounding
     * box
     * 
     * @param x
     *            the x coordinate of the upper-left corner of the arc to be
     *            drawn.
     * @param y
     *            the y coordinate of the upper-left corner of the arc to be
     *            drawn.
     * @param width
     *            the width of the arc to be drawn.
     * @param height
     *            the height of the arc to be drawn.
     * @param startAngle
     *            the beginning angle.
     * @param arcAngle
     *            the angular extent of the arc, relative to the start angle.
     */
    public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        g.drawArc(xTranslate + x, yTranslate + y, width, height, startAngle, arcAngle);
    }

    /**
     * Draw a string using the current font and color in the x,y coordinates.
     * The font is drawn from the top position and not the baseline.
     * 
     * @param str
     *            the string to be drawn.
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     */
    public void drawString(String str, int x, int y) {
        drawChars(str.toCharArray(), 0, str.length(), x, y);
    }

    void drawNativeChar(char character, int x, int y) {
        g.drawChar(character, xTranslate + x, yTranslate + y, g.TOP | g.LEFT);
    }

    // void drawNativeChars(javax.microedition.lcdui.Font font, char[] data, int
    // offset, int length, int x, int y) {
    // g.setFont(font);
    // g.drawChars(data, offset, length, xTranslate + x, yTranslate + y, g.TOP |
    // g.LEFT);
    // }

    /**
     * Draw the given char using the current font and color in the x,y
     * coordinates. The font is drawn from the top position and not the
     * baseline.
     * 
     * @param character
     *            - the character to be drawn
     * @param x
     *            the x coordinate of the baseline of the text
     * @param y
     *            the y coordinate of the baseline of the text
     */
    public void drawChar(char character, int x, int y) {
        current.drawChar(this, character, x, y);
    }

    /**
     * Draw the given char array using the current font and color in the x,y
     * coordinates. The font is drawn from the top position and not the
     * baseline.
     * 
     * @param data
     *            the array of characters to be drawn
     * @param offset
     *            the start offset in the data
     * @param length
     *            the number of characters to be drawn
     * @param x
     *            the x coordinate of the baseline of the text
     * @param y
     *            the y coordinate of the baseline of the text
     */
    public void drawChars(char[] data, int offset, int length, int x, int y) {
        current.drawChars(this, data, offset, length, x, y);
    }

    /**
     * Draws the image so its top left coordinate corresponds to x/y
     * 
     * @param img
     *            the specified image to be drawn. This method does nothing if
     *            img is null.
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     */
    public void drawImage(Image img, int x, int y) {
        img.drawImage(this, x, y);
    }

    void drawImage(VirtualImage img, int x, int y) {
        g.drawImage(img, xTranslate + x, yTranslate + y, g.TOP | g.LEFT);
    }

    /**
     * Draws an image with a MIDP trasnform for fast rotation
     */
    void drawImage(VirtualImage img, int x, int y, int transform) {
        if (transform != 0) {
            g.drawRegion(img, 0, 0, img.getWidth(), img.getHeight(), transform, xTranslate + x, yTranslate + y, g.TOP | g.LEFT);
        } else {
            drawImage(img, x, y);
        }
    }

    /**
     * Draws a filled triangle with the given coordinates
     * 
     * @param x1
     *            the x coordinate of the first vertex of the triangle
     * @param y1
     *            the y coordinate of the first vertex of the triangle
     * @param x2
     *            the x coordinate of the second vertex of the triangle
     * @param y2
     *            the y coordinate of the second vertex of the triangle
     * @param x3
     *            the x coordinate of the third vertex of the triangle
     * @param y3
     *            the y coordinate of the third vertex of the triangle
     */
    public void fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3) {
        g.fillTriangle(xTranslate + x1, yTranslate + y1, xTranslate + x2, yTranslate + y2, xTranslate + x3, yTranslate + y3);
    }

    /**
     * Draws the RGB values based on the MIDP API of a similar name. Renders a
     * series of device-independent RGB+transparency values in a specified
     * region. The values are stored in rgbData in a format with 24 bits of RGB
     * and an eight-bit alpha value (0xAARRGGBB), with the first value stored at
     * the specified offset. The scanlength specifies the relative offset within
     * the array between the corresponding pixels of consecutive rows. Any value
     * for scanlength is acceptable (even negative values) provided that all
     * resulting references are within the bounds of the rgbData array. The ARGB
     * data is rasterized horizontally from left to right within each row. The
     * ARGB values are rendered in the region specified by x, y, width and
     * height, and the operation is subject to the current clip region and
     * translation for this Graphics object.
     * 
     * @param rgbData
     *            an array of ARGB values in the format 0xAARRGGBB
     * @param offset
     *            the array index of the first ARGB value
     * @param scanlength
     *            the relative array offset between the corresponding pixels in
     *            consecutive rows in the rgbData array
     * @param x
     *            the horizontal location of the region to be rendered
     * @param y
     *            the vertical location of the region to be rendered
     * @param w
     *            the width of the region to be rendered
     * @param h
     *            the height of the region to be rendered
     * @param processAlpha
     *            true if rgbData has an alpha channel, false if all pixels are
     *            fully opaque
     */
    void drawRGB(int[] rgbData, int offset, int scanlength, int x, int y, int w, int h, boolean processAlpha) {

        int rgbX = xTranslate + x;
        int rgbY = yTranslate + y;

        // if the x or y are positive simply redirect the call to midp Graphics
        if (rgbX >= 0 && rgbY >= 0) {
            g.drawRGB(rgbData, offset, scanlength, rgbX, rgbY, w, h, processAlpha);
            return;
        }

        // first time try to draw with negative indexes
        if (drawNegativeOffsetsInRGB) {
            try {
                g.drawRGB(rgbData, offset, scanlength, rgbX, rgbY, w, h, processAlpha);
                return;
            } catch (RuntimeException e) {
                // if you failed it might be because you tried to paint with
                // negative
                // indexes
                drawNegativeOffsetsInRGB = false;
            }
        }

        // if the translate causes us to paint out of the bounds
        // we will paint only the relevant rows row by row to avoid some devices
        // bugs
        // such as BB that fails to paint if the coordinates are negative.
        if (rgbX < 0 && rgbX + w > 0) {
            if (scanlength < rgbData.length) {
                for (int i = 1; i <= rgbData.length / scanlength; i++) {
                    offset = -rgbX + (scanlength * (i - 1));
                    rgbY++;
                    if (rgbY >= 0) {
                        g.drawRGB(rgbData, offset, (scanlength + rgbX), 0, rgbY, scanlength + rgbX, 1, processAlpha);
                    }
                }
            }
        }
    }

    /**
     * Draws a radial gradient in the given coordinates with the given colors,
     * doesn't take alpha into consideration when drawing the gradient. Notice
     * that a radial gradient will result in a circular shape, to create a
     * square use fillRect or draw a larger shape and clip to the appropriate
     * size.
     * 
     * @param startColor
     *            the starting RGB color
     * @param endColor
     *            the ending RGB color
     * @param x
     *            the x coordinate
     * @param y
     *            the y coordinate
     * @param width
     *            the width of the region to be filled
     * @param height
     *            the height of the region to be filled
     */
    public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height) {
        int sourceR = startColor >> 16 & 0xff;
        int sourceG = startColor >> 8 & 0xff;
        int sourceB = startColor & 0xff;
        int destR = endColor >> 16 & 0xff;
        int destG = endColor >> 8 & 0xff;
        int destB = endColor & 0xff;
        int oldColor = getColor();
        int originalHeight = height;
        while (width > 0 && height > 0) {
            updateGradientColor(sourceR, sourceG, sourceB, destR, destG, destB, originalHeight, height);
            fillArc(x, y, width, height, 0, 360);
            x++;
            y++;
            width -= 2;
            height -= 2;
        }
        setColor(oldColor);
    }

    /**
     * Draws a linear gradient in the given coordinates with the given colors,
     * doesn't take alpha into consideration when drawing the gradient
     * 
     * @param startColor
     *            the starting RGB color
     * @param endColor
     *            the ending RGB color
     * @param x
     *            the x coordinate
     * @param y
     *            the y coordinate
     * @param width
     *            the width of the region to be filled
     * @param height
     *            the height of the region to be filled
     * @param horizontal
     *            indicating wheter it is a horizontal fill or vertical
     */
    public void fillLinearGradient(int startColor, int endColor, int x, int y, int width, int height, boolean horizontal) {
        // int sourceA = startColor >> 24 & 0xff;
        int sourceR = startColor >> 16 & 0xff;
        int sourceG = startColor >> 8 & 0xff;
        int sourceB = startColor & 0xff;
        // int destA = endColor >> 24 & 0xff;
        int destR = endColor >> 16 & 0xff;
        int destG = endColor >> 8 & 0xff;
        int destB = endColor & 0xff;
        int oldColor = getColor();
        if (horizontal) {
            for (int iter = 0; iter < width; iter++) {
                updateGradientColor(sourceR, sourceG, sourceB, destR, destG, destB, width, iter);
                drawLine(x + iter, y, x + iter, y + height);
            }
        } else {
            for (int iter = 0; iter < height; iter++) {
                updateGradientColor(sourceR, sourceG, sourceB, destR, destG, destB, height, iter);
                drawLine(x, y + iter, x + width, y + iter);
            }
        }
        setColor(oldColor);
    }

    private void updateGradientColor(int sourceR, int sourceG, int sourceB, int destR, int destG, int destB, int distance, int offset) {
        // int a = calculateGraidentChannel(sourceA, destA, distance, offset);
        int r = calculateGraidentChannel(sourceR, destR, distance, offset);
        int g = calculateGraidentChannel(sourceG, destG, distance, offset);
        int b = calculateGraidentChannel(sourceB, destB, distance, offset);
        int color = /* ((a << 24) & 0xff000000) | */((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | (b & 0xff);
        setColor(color);
    }

    /**
     * Converts the color channel value according to the offest within the
     * distance
     */
    private int calculateGraidentChannel(int sourceChannel, int destChannel, int distance, int offset) {
        if (sourceChannel == destChannel) {
            return sourceChannel;
        }
        float ratio = ((float) offset) / ((float) distance);
        int pos = (int) (Math.abs(sourceChannel - destChannel) * ratio);
        if (sourceChannel > destChannel) {
            return sourceChannel - pos;
        } else {
            return sourceChannel + pos;
        }
    }

    /**
     * Fills a rectangle with an optionally translucent fill color
     * 
     * @param x
     *            the x coordinate of the rectangle to be filled
     * @param y
     *            the y coordinate of the rectangle to be filled
     * @param w
     *            the width of the rectangle to be filled
     * @param h
     *            the height of the rectangle to be filled
     * @param alpha
     *            the alpha values specify semitransparency
     */
    public void fillRect(int x, int y, int w, int h, byte alpha) {
        int color = getColor();
        int alpahLevel = Display.getInstance().numAlphaLevels();

        int transparencyLevel = (int) alpha;

        if (alpahLevel <= 2) {
            if (transparencyLevel != 0) {
                transparencyLevel = 0xFF;
            }
        }

        // if component is opaque draw with rect using fillRect
        if (alpha == (byte) 0xFF) {
            g.setColor(color);
            g.fillRect(xTranslate + x, yTranslate + y, w, h);
            return;
        }
        // if component is fully opaque draw with rect using fillRect
        if (transparencyLevel == 0) {
            return;
        }

        transparencyLevel = transparencyLevel << 24;
        color = (color & 0x00FFFFFF);
        color = (color | transparencyLevel);

        if (rgbArr == null || rgbArr.length < w) {
            rgbArr = new int[w];
        }
        for (int i = 0; i < w; i++) {
            rgbArr[i] = color;
        }

        int rgbX = xTranslate + x;
        int rgbY = yTranslate + y;

        if (rgbX < 0 && rgbX + w > 0) {
            rgbX = 0;
            w = rgbX + w;
        }

        if (w < 0) {
            return;
        }

        for (int i = 0; i < h; i++) {
            if (rgbX >= 0 && rgbY + i >= 0) {
                g.drawRGB(rgbArr, 0, w, rgbX, rgbY + i, w, 1, true);
            }
        }

    }
}
